Some Behavioural Design Patterns
00 - Table of Contents
01 - Introduction
02 - Strategy
03 - Template Method
04 - Chain of Responsibility
05 - Observer or Publisher-Subscriber
06 - Other Behavioural Patterns
01 - Introduction
Everyone knows that the behaviour of an object is described by its methods. One could add that it is also described by the type of the object since a method named resize() doesn't not the same job in the Sprite class than in the Surface_Cache class.
The behaviour of the objects can be defined at run time with late binding mechanisms (provided by virtual methods in C++).
02 - Strategy
A Strategy basically defines a family of interchangeable algorithms.
The Strategy Pattern allows the coder the change the algorithm without having
to modify the client. As the following OMT figure shows, a reference to an
abstract strategy is hold in the client (also called Context in the scope of
this Pattern).
contains . --------- . -------- | Context |<>--------|Strategy| --------- -------- |. . . . . inherits /_\ +-------------------------+ | | | --------- --------- --------- |Strategy1| |Strategy2| |Strategy3| --------- --------- ---------This Pattern is very helpful when a large number of classes differs only by their behaviour. It also allows you to change the behaviour of a class in the time.
And when it comes to add a new behaviour to the context, you just have to write a new concrete Strategy without having to interfere in the core implementation of the client.
This pattern has been used in the Introduction article to design a simple image loading framework.
03 - Template Method
The goal of the Template Method Pattern is to define the skeleton of an
algorithm in one method with some parts of the algotihm are implemented by
sub-classes. Here's the example of a Texture Mapping class that use a
screen wide look-up table to find the (u,v) for each pixel on screen. This is
the kind of algorithm used for texture mapped tunnels and similar effects.
// The root class of all Texture_Mapper based effect class Texture_Mapper { // ... public: // ... void update( float time ); void draw( Surface *surface ); // here's the Template Method void calc_table(); protected: // primitive operations virtual void calc_setup(); virtual void calc_uv( float x, float y, int &u, int &v ); }; // Empty operations by default void Texture_Mapper::calc_setup() {}; void Texture_Mapper::calc_uv( float x,float y,int &u,int &v ) {} // of course, this is unoptimized example code... void Texture_Mapper::calc_table() { // call the initialization part of the algorithm calc_setup(); // for each pixel of a 320x240 screen for ( float y = -120; y < 120; y++ ) for ( float x = -160; x < 160; x++ ) { calc_uv( x, y, *utable, *vtable ); utable++, vtable++; } } // A simple and uncomplete example of Texture_Mapper class Tunnel : public Texture_Mapper { float rad; public: Tunnel( float radius ); void calc_uv( float x, float y, int &u, int &v ); }; // no need for setup code, we just keep the default behaviour void Tunnel::calc_uv( float x, float y, int &u, int &v ) { float xx = x+EPSILON; float a = atan(y/xx); u = int(fabs(cos(a)*rad/xx)); v = int(a*256.0/M_PI); }The previous example allows you to write an infinity of various Texture Mappers without having with the table calculation algorithm written only once.
To prevent sub-classes from overriding the Template Method, it is declared as non virtual.
With this Pattern, you only have to implement once the "static" parts of algorithm by factorizing the common behaviour in the parent class.
04 - Chain of Responsibility
The aim of this pattern is to delegate a request when class can't handle it.
calls +--------+ . | | ------ . ------- | |Client|------->|Handler|<>--+ ------ ------- | /_\ +-------------------------+ | | | -------- -------- -------- |Handler1| |Handler2| |Handler3| -------- -------- --------The client sends a request to a Handler inheriting from an abstract Handler that holds a reference to a successor handler to be used if we can't handle the request.
This pattern is often used with Composite. The chain used then is simply
the relations in the Composite pattern. Such a pattern is also often used
for event and error handling (rethrowing exception in the catch statement).
Some example code follows :
// Base Handler class Handler { Handler *succ; public: // Handler( Handler *s ) : succ(s) {} // Default behaviour of the handler virtual void handle() { if ( succ ) suff->handle(); } }; // Custom Handler class Handler1 : public Handler { public: Handler1( Handler *s ); void handle(); } Handler1::Handler1( Handler *s ) : Handler(s) { // Internal setup of custom handler ... } void Handler1::handle() { if ( i_can_handle_the_request ) // do the right stuff else Handler::handle(); }If you have several request, you can either write several methods or multiplex them in a single argument driven method.
When you send a request to a Chain of Responsibility, you don't know which class will process the query. Neither does the handling class knows which client sent the request.
05 - Observer or Publisher-Subscriber
This pattern allows an "Observable" (aka Master, Publisher) object to notify
one or more observers (aka Slaves, Subscribers) when its state changes.
--------------- ---------- | Publisher |----------O|Subscriber| |---------------| |----------| |att(Subscriber)| |callback()| - - - - abstract method |det(Subscriber)| ---------- |notify() | | --------------- | | | /_\ /_\ | | ---------- ----------- |Publisher1| |Subscriber1| |----------| |-----------| |state | |callback() | - - - - concrete method ---------- -----------On each modification of the state of the Publisher, its notify() method just invoque the callback() method of all the subscribed objects.
To avoid inheritance issues (i.e. notifying all the objects before updating the state in the child class), it's usually a good idea to design the notify() method as a template method (see section 03).
The Subscriber class will probably need information about the event that triggered the notification process. This may be done in basically two ways:
- the PUSH method: you pass all the information to the Subscriber, whatever it needs it or not. The advantage is that the communication protocol is simple but a large amount of data might have to be transferred.
- the PULL method: the Subscriber must request all the information it needs to accomplish its task. The protocol is more complex but it might be faster too.
The choice of the right method really depends on the context.
To increase the efficiency of the system, you can improve the granularity of the system by allowing the observers to subscribe only for a given set of events. This doesn't involve tremendous changes in the interface and can really speed things up.
Some might have noticed that this pattern is the cornerstone of the whole event system of the Java Abstract Windowing Toolkit 1.1.
This pattern can be used to notify clients when cached data (like surfaces in a 3D engine for instance) becomes outdated.
06 - Other Behavioural Patterns
There are much more Behavioural Patterns but I'm kinda busy for the moment and I miss time to cover them in this document. Drop me email if you want to see other Patterns developped in your favorite column!
Btw, cut-n-paste really rocks! =)
For you informations, here's a brief description of some other Behavioural Patterns:
- Command, when and how to process a request
- Iterator, how to wander in a container
- Interpreter, the grammar and the interpretation of a language
- Memento, which private is stored in the object and when
- Visitor, operations to apply to an object without modifying
its class